#! /usr/bin/env python
from multiprocessing import TimeoutError
import threading
import SocketServer
import time
import logging

dbg_lvl = logging.INFO
logger = logging.getLogger('gen_del')
logger.setLevel(dbg_lvl)
stream_handler =  logging.StreamHandler()
stream_handler.setLevel(dbg_lvl)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
#------------------------------------------------------------------------------
# Modes
RELAY               = 1 # forwarding mails to another mailserver
GDEL                = 2 # general-delivery notification mode
RETR                = 3 # retrival mode only
RECEIVER            = 4 # AUTH

DEBUG               = False     # turn this to true when testing on one machine
MODE                = RECEIVER  # current mode of the server
BUFFER_SIZE         = 1024      # defines input-buffer when reading from socket
FQDN                = 'envelope.com' # domain-name the server serves
HOST                = 'localhost'# ip-address the server listens to
PORT                = 25         # port the server listens: default 25
WELCOME_STRING      = '220 %s' % (FQDN)
CRNL = TERMINATOR   = '\r\n'
SPACE               = ' '
EMPTYSTRING         = ''
RECIPIENT_BUFFER    = 100
GDEL_STORAGE_PATH   = 'storage'
MAILBOX_PATH        = 'mailboxes'
SLEEP               = 2
TIMEOUT             = 20    # the timeout after which the server disconnects
                            # from a client due to inactivity
# internal state machine
COMMAND         = 1     # server is in receiving and processing commands mode
DATA            = 2     # server is in data-receiving mode
# SMTP-reply codes ------------------------------------------------------------
RC214 = '214-%s'
RC220 = '220 Ok'
RC221 = '221 See ya later!'
RC235 = '235 Authentication sucessful!'
RC250 = '250 Ok'
RC251 = '251 User not local'  # But we do not forward messages
RC252 = "252 Cannot VRFY user"
# positive intermediate reply -------------------------------------------------
RC334 = '334 %s'
RC354 = '354 Start mail input, end with <CRLF>.<CRLF>'
# transient negative completion reply
RC451 = '451 Internal Server Confusion'
RC452 = '452 Too many recipients'
RC490 = '490 Nothing to retrieve'
# permanent negative completion reply -----------------------------------------
RC500 = '500 Error'
RC501 = '501 Syntax: %s '
RC502 = '502 Command %s is not implemented!'
RC503 = '503 Bad sequence of commands!'
RC504 = '504 Unkown parameter'
RC535 = '535 Not authenticated!!!'
RC550 = '550 Requested action not taken!'
RC551 = '551 Unknown user.'
RC552 = '552 Out of memory!'
RC553 = '553 Requested action not taken'
RC554 = '554 Transaction failed!'
RC555 = '555 Parameter not recognized'

MX_LOOKUP = {}
A_LOOKUP = {}
#------------------------------------------------------------------------------
class RemoteSocketClosed(Exception):
    pass
#------------------------------------------------------------------------------
class TimeoutError(Exception):
    pass
#------------------------------------------------------------------------------
class ConnectionClosed(Exception):
    pass
#------------------------------------------------------------------------------
class NoSpf1Record(Exception):
    pass
#------------------------------------------------------------------------------
def query(name):
    import dns.resolver
    
    answer = dns.resolver.query(name, 'TXT')[0].strings[0]

    if answer is not None:
        if answer[:6] == 'v=spf1':
            answer = answer[7:]
            if ' a ' in answer.lower():
                answer = list()
                a_response = dns.resolver.query(name, 'A')
                if a_response:
                    for a in a_response:
                        answer.append('ip4:%s' % (a.address))
                aaaa_response = dns.resolver.query(name, 'AAAA')
                if aaaa_response:
                    for a in aaaa_response:
                        answer.append('ip6:%s' (a.address))
            if 'redirect:' in answer:
                answer = query(answer[9:])
            if 'include:' in answer:
                search = answer.split(' ')
                answer = list()
                for entry in search:
                    if 'include:' in entry:
                        i = entry.find(' ')
                        if i < 0: i = len(entry)
                        answer.extend(query(entry[8:i]))
                    else:
                        answer.append(entry)
            else:
                answer = answer.split(' ')
            return answer
        else:
            raise NoSpf1Record, 'no spf1 record'
    else: raise NoSpf1Record, 'no spf1 record'
#------------------------------------------------------------------------------
def check_spf(ehlo, ip):
    try:
        answer = query(ehlo)
    except NoSpf1Record:
        return False
    except:
        return False
    import ipcalc
    for entry in answer:
        isOk = False
        if entry[:4] == 'ip4:':
            isOk = ip in ipcalc.Network(entry)
        if entry[:4] == 'ip6:':
            isOk = ip in ipcalc.Network(entry)
        if entry == '-all':
            return False
        if isOk:
            return True
    return False
#------------------------------------------------------------------------------
def check_mailaddress(address):
    '''
    Checks if a given email-address is valid
    The address may be enclosed by angle brackets.
    Returns True if the address is valid
    '''
    logger.debug('email-address to check: %s' % (address))
    from email.utils import parseaddr
    import re

    match = re.match(
        "^<{0,1}[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}>{0,1}$",
        parseaddr(address)[1])
    if match:
        logger.debug('email-address is ok')
        return True
    else:
        logger.debug('email-address is not ok')
        return False
#------------------------------------------------------------------------------
def check_fqdn(fqdn):
    '''
    Checks if a given domain name exists
    Makes a dns-lookup for the A-Section of the domains record. If the address
    could be resolved and is valid it returns True, otherwise it returns False
    '''
    import dns.resolver
    try:
        answer = dns.resolver.query(fqdn, 'A')
        return True if answer else False
    except:
        return False
#------------------------------------------------------------------------------
def get_mx(name):
    '''
    Get the MX-record for a given domain-name or an email-address.
    Returns the address of the mail-server with the highest priority. If no
    MX entry could be found it returns None
    '''
    import dns.resolver
    logger.debug('get_mx() argument: %s' % (name))
    try:
        i = name.find('@')
        if i > 0:
            name = extract_domain(name)
            logger.debug('name: %s' % (name))
        answer = dns.resolver.query(name, 'MX')
        preference = 99999
        for record in answer:
            if record.preference < preference:
                preference = record.preference
                prefered_mx = record.exchange.to_text()
        if DEBUG: # this is only for debugging on one machine
            import socket
            return socket.gethostbyname(socket.gethostname())
        return prefered_mx[:len(prefered_mx)-1]
    except Exception as e:
        logger.debug(e)
        return None
#------------------------------------------------------------------------------
def extract_domain(address, check=True):
    '''
    Extracts the domain-name from an email-address
    If check=True, it also checks if the given email-address is valid by
    calling the check_mailaddress function.
    Returns the domainname
    '''
    if check:
        if not check_mailaddress(address):
            raise RuntimeError, 'Cannot extract from non-emailaddress %s' % \
                (address)
    import string
    return string.split(address, '@')[1]
#------------------------------------------------------------------------------
def serialize_email(email, path=None):
    import pickle
    if path == None:
        path = (('./%s/%s') % (GDEL_STORAGE_PATH, email.message_id))
    file = open(path, 'wb')
    pickle.dump(email, file)
    file.close()
#------------------------------------------------------------------------------
def deserialize_email(id, path=None):
    import pickle
    try:
        if path == None:
            path = (('./%s/%s') % (GDEL_STORAGE_PATH, id))
        file = open(path, 'rb')
        email = pickle.load(file)
        file.close()
        import os
        os.unlink(path)
        return email
    except IOError:
        logger.debug('Could not deserialize %s' % (path))
        return None
    except Exception as e:
        logger.debug(e)
        return None
#------------------------------------------------------------------------------
def insert_mail(email):
    try:
        for recipient in email.rcpt_to:
            mailbox_name = recipient.lower().strip('<>')
            import mailbox
            mailbox.Maildir.colon = '!'
            path = ('./%s/%s' % (MAILBOX_PATH, mailbox_name))
            logger.debug('path: %s' % (path))
            mailbox = mailbox.Maildir(path, create=False)
            mailbox.add(email.get_email_message())
    except mailbox.NoSuchMailboxError:
        logger.error('Unkown mailbox %s' % (mailbox_name))
    except Exception as e:
        logger.error(e)
#------------------------------------------------------------------------------
class SmtpRequestHandler(SocketServer.BaseRequestHandler):
    '''Handler for SMTP-requests'''
    def setup(self):
        self.handler = ConcreteSmtpRequestHandler((self.server,
            self.request, self.client_address))
        
    def handle(self):
        self.handler.handle()
#------------------------------------------------------------------------------
class ConcreteSmtpRequestHandler(object):

    id = 0

    def __init__(self, params, connected=False):
        self.id = ConcreteSmtpRequestHandler.id = \
            ConcreteSmtpRequestHandler.id + 1
        logger.debug('%d ConcreteSmtpRequestHandler: initialized' % (self.id))
        server, self.request, self.peer_name = (params)
        self.smtp_extensions = server.get_smtp_extensions()
        self.fqdn = FQDN                    # fqdn of the server
        self.smtp_listeners = set()         # set of listeners that get notified
        self.smtp_listeners.add(SmtpEvents(self))
        self.state = COMMAND                # internal state-machine
        self.data_buffer = list()           # stores incoming data 'til \r\n
        self.greeting = 'retrieving'        # args of ehlo-string
        self.__custom_vars = {}             # custom variables (optional)
        if not connected:
            self.push(WELCOME_STRING) # say hello
            logger.debug('New incoming connection from %s:%s' \
                % (self.peer_name))
            self.greeting = ''
        self.relay_queue = server.relay_queue # message-queue for threads
        self.start = time.time()
        self.mode = server.mode
        if self.mode == GDEL:
            self.smtp_listeners.add(GdelEvents(self))
            self.smtp_listeners.add(RetrEvents(self))
            self.gdel_queue = server.gdel_queue
            self.retr_queue = server.retr_queue
        logger.debug('%d CSRH:  # Smtp-listeners %d' \
            % (self.id, len(self.smtp_listeners)))
        from collections import deque
        self.__scheduler = deque()
        scheduler = self.__scheduler
        scheduler.append(self.process_incoming_data())
        scheduler.append(self.receive_incoming_data())

    def finish(self):
        '''Called when connection closes.'''
        del (self.data_buffer)

    def handle(self):
        '''Handle all incoming requests from one connection'''
        logger.debug('Handler called')
        scheduler = self.__scheduler #copying reference for better performance
        try:
            while scheduler:
                task = scheduler.pop()
                next(task)
                scheduler.appendleft(task)
        except ConnectionClosed:
            logger.debug('%d Connection closed' % (self.id))
        except Exception as e:
            logger.error('%d %s' % (self.id, e))

    def receive_incoming_data(self):
        '''Looks for data available at the socket.
        If data is available, the handle-event is notified.
        '''
        from select import select
        while True:
            r, _, _ = select([self.request], [], [], 0.005)
            if time.time() - self.start > TIMEOUT:
                raise TimeoutError, 'Timed out after %d seconds' % (TIMEOUT)
            if r:
                data_string = self.request.recv(1024)
                self.data_buffer.append(data_string)
                if len(data_string) == 0:
                    raise RemoteSocketClosed, \
                        'remote socket unexpectedly closed'
                yield

    def process_incoming_data(self):
        '''Process the data in the internal databuffer
        Checks the internal databuffer if there is an terminator found and
        calls the found_terminator() function.
        '''
        while True:
            pos = len(self.data_buffer)-1
            if TERMINATOR in self.data_buffer[pos]:
                logger.debug('%d Terminator found' % (self.id))
                if self.state == COMMAND: # delete terminator only for commands
                    i = self.data_buffer[pos].find(TERMINATOR)
                    self.data_buffer[pos] = self.data_buffer[pos][:i]
                self.found_terminator()
                self.flush_buffer()
            yield

    def push(self, msg):
        '''sends a message to the socket'''
        logger.debug('%d push>> %s' % (self.id, msg))
        self.request.send(('%s %s') % (msg, CRNL))
        
    def flush_buffer(self):
        '''well, flushes the buffer...'''
        logger.debug('%d flush!' % (self.id))
        del(self.data_buffer)
        self.data_buffer = list()

    def found_terminator(self):
        '''Parses the data-buffer for commands'''
        line = EMPTYSTRING.join(self.data_buffer)
        logger.debug('%d internal state is %s' % (self.id, self.state))
        self.start = time.time()
        if self.state == COMMAND:
            if not line: # just received a \r\n
                self.push(RC500) # send errormessage
                return
            i = line.find(SPACE)
            if i < 0: # no arguments found
                command = line.lower()
                arguments = None
            else:
                command = line[:i].lower()
                arguments = line[i+1:].strip()
            for listener in self.smtp_listeners:
                success = listener.notify(command, arguments)                
            if not success:
                self.push(RC500)
        elif self.state == DATA: # incoming data should be message-content
            for listener in self.smtp_listeners:
                listener.notify('data', line)
        else:
            # internal server confusion
            logger.warning('Internal Server confusion!')
            self.state = COMMAND # put the state back to COMMAND
            self.push(RC451)

    def process_email(self, email):
        """Gets called when an email transmition has been finished.
        The function decides what to do next. Depending on the mode in which
        the server is running it notifies the dispatching thread.
        """
        logger.debug('%d ehlo: %s' % (self.id, email.ehlo))
        logger.debug('peer_name: %s %s' % (email.peer_name))
        logger.debug('%d mail_from: %s' % (self.id, email.mail_from))
        logger.debug('%d, rcpt_to: %s' % (self.id, ','.join(email.rcpt_to)))
        logger.debug(email.as_string())
        try:
            if self.mode == GDEL:
                self.gdel_queue.put(email, True, 10)
            elif self.mode == RELAY:
                self.relay_queue.put(email, True, 10)
            else:
                insert_mail(email)
            return True
        except Exception as e:
            logger.error('%d %s' % (self.id, e))
            return False

    def close(self):
        '''Closes the current connection'''
        logger.debug('%d Closing connection...' % (self.id))
        self.request.close()
        raise ConnectionClosed, 'Connection closed'

    def set_var(self, key, val):
        '''Allows a custom value to be stored in a dictionary'''
        self.__custom_vars[key] = val

    def get_var(self, key):
        '''Allows a stored custom value to be retrieved'''
        try:
            return self.__custom_vars[key]
        except KeyError:
            return None
#------------------------------------------------------------------------------
class EmailMessage(object):

    def __init__(self):
        self.mail_from = '' # return-path/sender's address
        self.rcpt_to = set() # set of all recipients
        self.mail_data = list() # the actual maildata (in list form)
        self.ehlo = '' # the ehlo-string of the sending mailserver
        self.peer_name = '' # the ip-address of the sending mailserver
        self.message_id = self._create_id()
        self.attempt = 0

    def _create_id(self):
        from hashlib import sha1
        from time import time
        from random import randint
        id = sha1()
        id.update(str(time()))
        id.update(str(randint(1, 1000)))
        return id.hexdigest()

    def get_email_message(self):
        from email import message_from_string
        email_message = message_from_string(EMPTYSTRING.join(self.mail_data))
        email_message = self.add_headers(email_message)
        return email_message

    def as_string(self):
        return self.get_email_message().as_string()

    def add_headers(self, msg):
        '''Adds header-strings to an Email-message

        The msg has to be an email.message.Message as described in the Python
        Standard Library 19.1.1
        '''
        from time import strftime, localtime
        received = \
           ('from %s (ehlo: %s [%s]) by Envelope [%s] with msg-id %s@%s, %s' \
         % (self.mail_from, self.ehlo, self.peer_name, HOST, self.message_id, \
            FQDN, strftime("%a, %d %b %Y %H:%M:%S", localtime()) ))
        msg['Message-ID'] = '<%s@%s>' % (self.message_id, FQDN)
        msg['Received'] = received
        return msg
#------------------------------------------------------------------------------
class NotificationStub(object):
    
    def __init__(self, uid, peer_info):
        self.uid = uid
        # peer_info => tuple(ip-address, ehlo_name)
        self.peer_info = peer_info
#------------------------------------------------------------------------------
class SmtpDispatcher(threading.Thread):

    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue

    def run(self):
        from time import sleep
        while True:
            sleep(0.2)
            try:
                item = self.queue.get()
                if item == 'shutdown': break
                logger.debug('processing something')
                self.process(item)
            except:
                pass
#------------------------------------------------------------------------------
class Relayer(SmtpDispatcher):
    
    def __init__(self, queue):
        SmtpDispatcher.__init__(self, queue)
        logger.debug('Relayer: initialized')
  
    def process(self, item):
        logger.debug('Relayer: processing....')
        from smtplib import SMTP
        rcpt = item.rcpt_to
        for recipient in rcpt:
            dns_sema.acquire()
            addr = get_mx(recipient)
            dns_sema.release()
            try:
                conn = SMTP(addr)
                conn.sendmail(item.mail_from, recipient, item.as_string())
            except Exception as e:
                #todo handling
                logger.error(e)
#------------------------------------------------------------------------------
class Notifier(SmtpDispatcher):
    
    def __init__(self, queue):
        SmtpDispatcher.__init__(self, queue)
        logger.debug('Notifier: initialized')

    def process(self, item):
        logger.debug('Notifier: processing....')
        rcpt = item.rcpt_to
        from smtplib import SMTP
        for recipient in rcpt:
            dns_sema.acquire()
            addr = get_mx(recipient)
            dns_sema.release()
            try:
                conn = SMTP(addr, PORT)
                conn.set_debuglevel(1)
                conn.ehlo(FQDN)
                if conn.has_extn('gdel'):
                    serialize_email(item)
                    rc, _ = conn.docmd('GDEL %s@$s' % (item.message_id, FQDN))
                    # TODO
                    if rc == 250:
                        # everything is ok
                        logger.debug('Notifier: %s notified' % (addr))
                    elif rc == 500: # the receiver does not recognize GDEL
                        logger.error(
                            'Notifier: %s does not recognize GDEL' % (addr))
                    elif rc == 502: # the receiver does not support GDEL
                        logger.error(
                            'Notifier: %s does not support GDEL' % (addr))
                else:
                    logger.warning(
                        'Notifier: %s does not support GDEL' % (addr))
                conn.quit()
                # implement fallback?
            except Exception as e: #something went wrong... but what?
                logger.error('Notifier: %s %s' % (e.__class__, e))
                pass
#------------------------------------------------------------------------------
class _DummyServerMixin():

    mode = RETR
    relay_queue = None
    gdel_queue = None

    def get_smtp_extensions(self):
        return set()
#------------------------------------------------------------------------------
class ConcreteRetriever(threading.Thread, _DummyServerMixin):
    
    def __init__(self, item):
        threading.Thread.__init__(self)
        self.item = item
        logger.debug('ConcreteRetriever initialized')

    def run(self):
        from time import sleep
        from random import randint
        sleep(SLEEP * randint(1, 100))
        item = self.item
        uid = item.uid
        logger.debug('Retrieving %s' % (uid))
        ip, ehlo = (item.peer_info)
        uid_id, uid_fqdn = uid.split('@')
        try:
            if check_spf(uid_fqdn, ip[0]):
                from smtplib import SMTP
                conn = SMTP(ip[0], PORT)
                conn.set_debuglevel(1)
                conn.ehlo(FQDN)
                if conn.has_extn('retr'):
                    logger.debug('Retriever: Remote host supports RETR.')
                    rc, _ = conn.docmd('RETR', uid)
                    if rc == 250:
                        logger.debug('Retriever: Remote host found %s' % (uid))
                        handler = ConcreteSmtpRequestHandler(
                            (self, conn.sock, ip), True)
                        handler.handle()
                    else:
                        logger.error('Retriever: Could not retrieve message')
            else:
                logger.error('Retriever: Remote host has no valid SPF1')
        except Exception as e:
            logger.error('Retriever: %s %s' % (e.__class__, e))
            logger.error('Retriever: something went horribly wrong')
            if conn:
                conn.docmd('500', 'Error')
#------------------------------------------------------------------------------
class Retriever(SmtpDispatcher):

    def __init__(self, queue):
        SmtpDispatcher.__init__(self, queue)
        logger.debug('Retriever: initialized')
        self.retr_queue = queue

    def process(self, item):
        retriever = ConcreteRetriever(item)
        retriever.start()
#------------------------------------------------------------------------------
class SmtpServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):

    smtp_extensions = set()
    mode = ''

    def _register_smtp_extensions(self):
        if self.mode == GDEL:
            self.smtp_extensions.add('GDEL')
            self.smtp_extensions.add('RETR')

    def get_smtp_extensions(self):
        return self.smtp_extensions

    def set_mode(self, mode):
        self.mode = mode

    def init(self, mode):
        self.set_mode(mode)
        self._init_queues()
        self._register_smtp_extensions()
        self._spawn_dispatcher()

    def _init_queues(self):
        from Queue import Queue
        logger.debug('Initializing Queues')
        self.relay_queue = Queue(10)
        if self.mode == GDEL:
            self.gdel_queue = Queue(10)
            self.retr_queue = Queue(10)

    def _spawn_dispatcher(self):
        if self.mode == RELAY:
            logger.debug('Spawning relayer')
            relayer = Relayer(self.relay_queue)
            relayer.start()
        elif self.mode == GDEL:
            logger.debug('Spawning notifier and retriever')
            notifier = Notifier(self.gdel_queue)
            retriever = Retriever(self.retr_queue)
            notifier.start()
            retriever.start()
        else:
            logger.error('Could not spawn dispatcher. No/Unknown mode %s' %
                            (self.mode))
   
    def shutdown(self):
        logger.debug('Shutting down the SMTP-server')
        if self.mode == RELAY: 
            self.relay_queue.put('shutdown')
        if self.mode == GDEL:
            self.retr_queue.put('shutdown')
            self.gdel_queue.put('shutdown')
        SocketServer.TCPServer.shutdown(self)
#------------------------------------------------------------------------------
class Events(object):
    
    def __init__(self, handler):
        self.handler = handler
        self.email = EmailMessage()
        
    def reset(self):                                   # issue 1, nat@11.1.2010
        logger.debug('%d Reset' % (self.handler.id))
        self.handler.state = COMMAND
        self.email = ''
        self.email = EmailMessage()


    def notify(self, cmd, arg):
        logger.debug('Command: "%s" Arguments: "%s"' % (cmd, arg))
        event = getattr(self, 'on_'+cmd, None)
        if event:
            event(arg)
            return True
        else:
            #logger.debug('bad request: %s' % (cmd))
            return False

    def on_ehlo(self, arg): pass
    def on_helo(self, arg): pass
    def on_noop(self, arg): pass
    def on_rset(self, arg): pass
    def on_mail(self, arg): pass
    def on_rcpt(self, arg): pass
    def on_data(self, arg): pass
    def on_gdel(self, arg): pass
    def on_retr(self, arg): pass
    def on_quit(self, arg): pass
    def on_help(self, arg): pass
    def on_vrfy(self, arg): pass
#------------------------------------------------------------------------------
class SmtpEvents(Events):

    def __init__(self, handler):
        Events.__init__(self, handler)

    def on_helo(self, arg):
        if not arg:
            self.handler.push(RC501 % ('HELO hostname!'))
            #envLog.info('Wrong HELO-syntax')
            return
        else:                
            self.handler.greeting = arg
            self.handler.push('250 %s' % (self.handler.fqdn))
            self.handler.legacy = True

    def on_ehlo(self, arg):
        if not arg:
            self.handler.push(RC501 % ('EHLO hostname!'))
            return
        else:
            if self.handler.greeting:
                # according to RFC5321 section 4.1.4 an EHLO-command issued
                # later in a session has the same effect as the RSET command.
                # This means that all information stored so far has to be
                # cleared and set back.
                self.handler.reset()
            self.handler.greeting = arg
            extensions = self.handler.smtp_extensions
            reply = ('250-%s whazzup?' % (self.handler.fqdn))
            if len(extensions) > 0:
                # according to RFC5321 section 4.2.1 a multiline reply requires
                # every line exept the last one to begin with a hyphen after
                # the reply-code
                for extension in extensions:
                    reply = reply + CRNL + ('250-%s' % (extension))
            reply = reply + CRNL + ('250 HELP')
            self.handler.push(reply)

    def on_noop(self, arg):
        self.handler.push(RC250)

    def on_quit(self, arg):
        self.handler.push(RC221) # say goodbye
        self.handler.close()

    def on_rset(self, arg):
        if arg:
            self.handler.push(RC501 % ('RSET <CRLF>'))
        else:
            del(self.email)
            self.email = EmailMessage()
            self.handler.reset()
            self.handler.push(RC250)

    def on_mail(self, arg):
        if self.handler.greeting == '':
            self.handler.push(RC503)
            logger.debug('bad sequence of commands')
            return # exit this
        if arg[:5].lower() == 'from:':
            arg = arg[5:].strip()
        else:
            self.handler.push(RC504)
            logger.debug('got a crappy command')
            return # exit this
        if self.handler.state == COMMAND:
            if self.email.mail_from == '':
                import string
                argList = string.split(arg, ' ')
                # check if this is a correct email-address
                # and then check if the domain of the reply-path is valid

                if check_mailaddress(argList[0]):
                    domainName = string.split(argList[0], '@')[1]
                    if domainName[len(domainName)-1:] == '>':
                        domainName = domainName[:len(domainName)-1]
                    if check_fqdn(domainName):
                        self.email.mail_from = argList[0]
                        self.email.peer_name = self.handler.peer_name
                        self.email.ehlo = self.handler.greeting
                        self.handler.from_param = argList[1:]
                        self.handler.push(RC250)
                    else:
                        self.handler.push(RC553)
                else:
                    self.handler.push(RC503)
            else:
                logger.debug('Bad sequence - multiple mail from')
                self.handler.push(RC503)
        else:
            self.handler.push(RC503)

    def on_rcpt(self, arg):
        if self.handler.greeting == '':
            self.handler.push(RC503)
            return # exit this
        if arg[:3].upper() == 'TO:':
            arg = arg[3:].strip()
        else:
            self.handler.push(RC504)
            return # exit this
        if self.handler.state == COMMAND:
            if not self.email.mail_from == '':
                if len(self.email.rcpt_to) >= RECIPIENT_BUFFER:
                    self.handler.push(RC452)
                else:
                    self.email.rcpt_to.add(arg)
                    self.handler.push(RC250)
                    logger.debug('Number of recipients: %s'
                        % (len(self.email.rcpt_to)))
            else:
                self.handler.push(RC503)
        else:
            self.handler.push(RC503)

    def on_data(self, arg):
        if self.handler.state == COMMAND:
            if self.email.mail_from == '':
                self.handler.push(RC503)
            else:
                self.handler.state = DATA
                self.handler.push(RC354)
                logger.debug('switched to DATA-state, ready for input')
        elif self.handler.state == DATA:
            logger.debug('DATA %s' % (arg))
            if (arg and arg[0] == '.') or arg[len(arg)-5:] == '\r\n.\r\n':
                self.email.mail_data.append(arg)
                if self.handler.process_email(self.email) == True:
                    self.handler.push(RC250)
                else:
                    self.handler.push(RC554)
                self.reset()                           # issue 1, nat@11.1.2010
            else:
                self.email.mail_data.append(arg)

    def on_help(self, arg):
        self.handler.push(RC214 % ('This is envelope-mail v 0.1'))
        self.handler.push(RC214 % ('Supported commands:'))
        self.handler.push(RC214 % ('    HELO    EHLO     NOOP     MAIL'))
        self.handler.push(RC214 % ('    RCPT    RSET     DATA     VRFY'))
        if self.handler.mode == GDEL:
            self.handler.push(RC214 % ('HELP    GDEL    RETR'))
        else:
            self.handler.push(RC214 % ('    HELP'))
        self.handler.push('214 End of HELP info')

    def on_vrfy(self, arg):
        self.handler.push(RC252)
#------------------------------------------------------------------------------
class GdelEvents(Events):

    def __init__(self, handler):
        Events.__init__(self, handler)
        
    def on_gdel(self, arg):
        logger.debug('gdel fired')
        if not arg:
            self.handler.push(RC501 % ('GDEL <uid>@<FQDN><CRLF>'))
        else:
            # put it into the retrievequeue
            if not '@' in arg:
                self.handler.push(RC501 % ('GDEL <uid>@<FQDN><CRLF>'))
            else:
                stub = NotificationStub(arg,
                    (self.handler.peer_name, self.handler.greeting))
                self.handler.retr_queue.put(stub)
                self.handler.push(RC250)
#------------------------------------------------------------------------------
class RetrEvents(Events):

    def __init__(self, handler):
        Events.__init__(self, handler)

    def on_retr(self, arg):
        if not arg:
            self.handler.push(RC501 % ('RETR <uid><CFLF>'))
        else:
            # forward the mail
            logger.debug('RetrEvents: got %s to retrieve' % (arg))
            email = deserialize_email(arg.split('@')[0])
            if email == None:
                logger.debug('RetrEvents: could not find %s' % (arg))
                self.handler.push(RC490)
            else:
                logger.debug('RetrEvents: %s found' % (arg))
                self.handler.push(RC250)
                from smtplib import SMTP
                class RetrSmtp(SMTP):
                    def __init__(self, socket):
                        SMTP.__init__(self)
                        self.sock = socket
                        self.ehlo_resp = 'retrieve'
                rcpt = email.rcpt_to
                for recipient in rcpt:
                    try:
                        conn = RetrSmtp(self.handler.request)
                        conn.set_debuglevel(1)
                        conn.sendmail(email.mail_from,
                            recipient, email.as_string())
                        conn.quit()
                    except Exception as e:
                    # todo: serialize it here
                        print e
                        logger.error('RetrEvents: Something went ' /
                            'wrong when retrieving a message')
#------------------------------------------------------------------------------
def smtp(mode=RELAY,fqdn=FQDN, host=HOST, port=PORT):
    server = SmtpServer((host, port), SmtpRequestHandler)
    server.init(mode)
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.setDaemon(True)
    server_thread.start()
    logger.info('Serving at %s:%d' % (host, port))
    return server

if __name__ == "__main__":
    from optparse import OptionParser
    import socket
    dns_sema = threading.BoundedSemaphore(5)
    host = socket.gethostbyname(socket.gethostname())
    mode = MODE
    port = PORT

    parser = OptionParser()
    parser.add_option('-m', '--mode', action='store', type='string',
        dest='mode', help='The mode in which the server is started. Possible' \
        + 'options are relay, gdel')
    parser.add_option('--host', action='store', type='string',
        dest='host', help='ip-address on which the smtp-server listens')
    parser.add_option('-p', '--port', action='store', type='int',
        dest='port', help='port on which the server listens. Defaults to 25')
    parser.add_option('--ehlo', action='store', type='string',
        dest='ehlo', help='This is how the server indentifies itself with ehlo')
    parser.add_option('-d', action='store_true', dest='debug')

    (options, _) =parser.parse_args()

    if options.mode != None:
        option = options.mode.strip().upper()
        if option not in ['GDEL', 'RELAY', 'RECEIVER']:
            print ('Error: unkown mode %s' % (options.mode))
            parser.print_help()
            exit()
        else:
            if option == 'GDEL':
                mode = GDEL
            elif option == 'RELAY':
                mode = RELAY
            elif option == 'RECEIVER':
                mode = RECEIVER
            else:
                print ('Error: unkown mode %s' % (options.mode))
                parser.print_help()
                exit()

    if options.host:
        host = options.host

    if options.port:
        port = options.port

    if options.ehlo:
        FQDN = options.ehlo
        WELCOME_STRING = ('220 %s' % (FQDN))

    if options.debug:
        DEBUG = True

    serv = smtp(mode=mode, host=host, port=port)
    while True:
        try:
           time.sleep(10)
        except KeyboardInterrupt:
            break
    print 'Interrupt'
    serv.shutdown()